package ru.lukyanets.transmission.client.connection;

import com.google.inject.Singleton;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import ru.lukyanets.transmission.protocol.client.QueueAction;
import ru.lukyanets.transmission.protocol.client.RpcClient;
import ru.lukyanets.transmission.protocol.client.TorrentAction;
import ru.lukyanets.transmission.protocol.client.TorrentFileMarker;
import ru.lukyanets.transmission.protocol.client.builder.RestRpcClientBuilder;
import ru.lukyanets.transmission.protocol.client.exception.TransmissionException;
import ru.lukyanets.transmission.protocol.model.session.Session;
import ru.lukyanets.transmission.protocol.model.session.stats.SessionStats;
import ru.lukyanets.transmission.protocol.model.torrent.Torrent;
import ru.lukyanets.transmission.protocol.model.torrent.arguments.TorrentSetArgs;

import java.util.List;

/**
 * @author slukyanets
 */
@Singleton
public class TransmissionService implements RpcClient, ObservableValue<ConnectionState> {
    private static final int DEFAULT_TIMEOUT = 1000;
    private static final int DEFAULT_MAX_ATTEMPTS = 3;

    private int retryCount = 0;
    private int maxRetries = Integer.MAX_VALUE; // TODO add to config
    private long retryInterval = 5000L; // TODO add to config

    private final ObjectProperty<ConnectionState> state = new SimpleObjectProperty<>(ConnectionState.DISCONNECTED);
    private RpcClient client;
    private ConnectionInfo connectionInfo;

    private Thread monitorThread;
    private final Object mutex = new Object();

    /**
     * Starts a remote connection.
     */
    public void connect(ConnectionInfo connectionInfo) {
        this.retryCount = 0;
        this.connectionInfo = connectionInfo;
        if (getState() != ConnectionState.DISCONNECTED) {
            disconnect();
        }
        synchronized (mutex) {
            RestRpcClientBuilder builder = new RestRpcClientBuilder().maxAttempts(DEFAULT_MAX_ATTEMPTS).timeout(DEFAULT_TIMEOUT);
            client = builder.host(connectionInfo.getHost())
                    .useSsl(connectionInfo.isUseSsl())
                    .proxyHost(connectionInfo.getProxyHost())
                    .alternativePath(connectionInfo.getAlternativePath()).build();

            setConnectionState(ConnectionState.CONNECTING);

            monitorThread = new Thread(new ConnectionMonitor());
            monitorThread.setDaemon(true);
            monitorThread.start();
        }
    }

    /**
     * Terminates a remote connection.
     */
    public void disconnect() {
        Thread copy;
        synchronized (mutex) {
            // not currently connecting or connected
            if (monitorThread == null) {
                return;
            }
            client = null;
            mutex.notifyAll();
            copy = monitorThread;
        }
        // we should join monitor thread outside of synchronized block to avoid dead-lock
        if (copy != null) {
            try {
                copy.join();
            } catch (InterruptedException e) {
                /* do nothing */
            }
        }
    }

    public ConnectionInfo getConnectionInfo() {
        return new ConnectionInfo(connectionInfo);
    }

    private void setConnectionState(ConnectionState state) {
        // we always enter this function from within a synchronized block
        if (getState() == state) {
            /* do nothing */
            return;
        }
        // update state
        this.state.set(state);
    }

    public ConnectionState getState() {
        return state.get();
    }

    @Override
    public List<Torrent> getTorrents() throws TransmissionException {
        return client.getTorrents();
    }

    @Override
    public Torrent getTorrent(int id) throws TransmissionException {
        return client.getTorrent(id);
    }

    @Override
    public void setTorrent(TorrentSetArgs arguments) throws TransmissionException {
        client.setTorrent(arguments);
    }

    @Override
    public void actionTorrent(List<Integer> ids, TorrentAction action) throws TransmissionException {
        client.actionTorrent(ids, action);
    }

    @Override
    public int addTorrent(String metainfo, List<TorrentFileMarker> files, String cookies, String downloadDir, boolean paused, int peerLimit, int bandwidthPriority) throws TransmissionException {
        return client.addTorrent(metainfo, files, cookies, downloadDir, paused, peerLimit, bandwidthPriority);
    }

    @Override
    public void removeTorrent(List<Integer> ids, boolean deleteLocalData) throws TransmissionException {
        client.removeTorrent(ids, deleteLocalData);
    }

    @Override
    public void setLocationTorrent(List<Integer> ids, String location, boolean move) throws TransmissionException {
        client.setLocationTorrent(ids, location, move);
    }

    @Override
    public void renamePathTorrent(List<Integer> ids, String path, String name) throws TransmissionException {
        client.renamePathTorrent(ids, path, name);
    }

    @Override
    public void actionQueue(List<Integer> ids, QueueAction action) throws TransmissionException {
        client.actionQueue(ids, action);
    }

    @Override
    public boolean testPort() throws TransmissionException {
        return client.testPort();
    }

    @Override
    public Session getSession() throws TransmissionException {
        return client.getSession();
    }

    @Override
    public void setSession(Session session) throws TransmissionException {
        client.setSession(session);
    }

    @Override
    public void shutdownServer() throws TransmissionException {
        client.shutdownServer();
    }

    @Override
    public SessionStats getSessionStats() throws TransmissionException {
        return client.getSessionStats();
    }

    @Override
    public void updateBlocklist() throws TransmissionException {
        client.updateBlocklist();
    }

    @Override
    public void addListener(ChangeListener<? super ConnectionState> listener) {
        state.addListener(listener);
    }

    @Override
    public void removeListener(ChangeListener<? super ConnectionState> listener) {
        state.removeListener(listener);
    }

    @Override
    public ConnectionState getValue() {
        return getState();
    }

    @Override
    public void addListener(InvalidationListener listener) {
        state.addListener(listener);
    }

    @Override
    public void removeListener(InvalidationListener listener) {
        state.removeListener(listener);
    }

    private class ConnectionMonitor implements Runnable {
        @Override
        public void run() {
            synchronized (TransmissionService.this.mutex) {
                while (TransmissionService.this.client != null) {
                    try {
                        try {
                            TransmissionService.this.client.getSession();
                            setConnectionState(ConnectionState.CONNECTED);
                            TransmissionService.this.mutex.wait(500L);
                        } catch (TransmissionException e) {
                            if(retryCount < maxRetries) {
                                retryCount++;
                                setConnectionState(ConnectionState.CONNECTING);
                                TransmissionService.this.mutex.wait(retryInterval);
                            } else {
                                TransmissionService.this.client = null;
                            }
                        }
                    } catch (InterruptedException ignored) {}
                }

                TransmissionService.this.setConnectionState(ConnectionState.DISCONNECTED);
            }
        }
    }
}
